import { isBuffer, isMap, isSet, isTypedArray, once } from 'lodash-es'
import { Field1, Field2, Field3, NoRecursion_Flag } from './symbols'

let registerOnExit: (listener: () => any) => void
let unregisterOnExit: (listener: () => any) => void

if (
  /* #__PRUE__ */ typeof window !== 'undefined' &&
  typeof window.addEventListener === 'function'
) {
  registerOnExit = /* #__PRUE__ */ (listener: () => any) =>
    window.addEventListener('unload', listener)
  unregisterOnExit = /* #__PRUE__ */ (listener: () => any) =>
    window.removeEventListener('unload', listener)
} else if (/* #__PRUE__ */ typeof process !== 'undefined' && typeof process.on === 'function') {
  registerOnExit = /* #__PRUE__ */ (listener: () => any) => process.on('exit', listener)
  unregisterOnExit = /* #__PRUE__ */ (listener: () => any) => process.off('exit', listener)
} else if (/* #__PRUE__ */ typeof self !== 'undefined') {
  registerOnExit = /* #__PRUE__ */ (listener: () => any) =>
    self.addEventListener('unload', listener)
  unregisterOnExit = /* #__PRUE__ */ (listener: () => any) =>
    self.removeEventListener('unload', listener)
} else {
  registerOnExit = unregisterOnExit = () => {}
}

export abstract class ChannelConnection {
  abstract get disconnected(): boolean
  abstract get connected(): boolean
  abstract whenConnected(): Promise<void>
  abstract whenDisconnected(): Promise<void>
  abstract _sendObject(obj: any): void
  protected abstract _close(): void

  emitObject(obj: any) {
    // @ts-ignore
    this[Field1]?.[Field3](obj)
  }
  emitError(...args: any[]) {
    // @ts-ignore
    this[Field1]?.[Field2]('error', args)
  }
  close() {
    try {
      // @ts-ignore
      this[Field1]?.disconnect(NoRecursion_Flag)
      this._close()
    } catch (e) {}
  }
}

interface TransformOptions {
  jsonReplacer?: (this: any, key: string, value: any) => any
  jsonReviver?: (this: any, key: string, value: any) => any
  stringify?: (obj: any) => string
  parse?: (string: string) => any
}

function defaultReplacer(this: any, key: string, value: any) {
  if (typeof value === 'string') {
    return value
  } else if (isSet(value) || isMap(value) || isTypedArray(value) || isBuffer(value)) {
    return Array.from(value)
  } else if (value instanceof Error) {
    return {
      ...value,
      name: value.name,
      message: value.message,
      stack: value.stack,
    }
  }
}

const resolveTransformOptions = (options?: TransformOptions) => {
  const _jsonReplacer = options?.jsonReplacer
  const _jsonReviver = options?.jsonReviver
  const jsonReplacer = _jsonReplacer
    ? function (this: any, key: any, value: any) {
        value = _jsonReplacer.call(this, key, value)
        value = defaultReplacer.call(this, key, value)
        return value
      }
    : _jsonReplacer
  const jsonReviver = _jsonReviver

  const parse = options?.parse ?? ((string) => JSON.parse(string, jsonReviver))
  const stringify = options?.stringify ?? ((obj: any) => JSON.stringify(obj, jsonReplacer))

  return {
    parse,
    stringify,
  }
}

function _baseConstruct(this: ChannelConnection) {
  let resolveConnected: () => void
  let resolveDisconnected: () => void
  let _resolveConnected: () => void
  let _resolveDisconnected: () => void

  let connected = false
  let disconnected = false
  Object.defineProperties(this, {
    disconnected: {
      get: () => disconnected,
    },
    connected: {
      get: () => connected,
    },
  })

  let connectedPromise = new Promise<void>((resolve) => {
    _resolveConnected = resolve
    resolveConnected = once(() => {
      resolve()
      connected = true
    })
  })
  let disConnectedPromise = new Promise<void>((resolve) => {
    _resolveDisconnected = resolveDisconnected
    resolveDisconnected = once(() => {
      _resolveConnected()
      connectedPromise.then(() => {
        resolve()
        connected = false
        disconnected = true
      })
    })
  })

  this.whenConnected = () => connectedPromise
  this.whenDisconnected = () => disConnectedPromise

  return [connectedPromise, resolveConnected!, disConnectedPromise, resolveDisconnected!] as const
}

// @ts-ignore
type SockJsWebSocket = import('sockjs').Connection

// @ts-ignore
type NativeWebSocket = WebSocket

const { CLOSED: WebSocket_CLOSED, OPEN: WebSocket_OPEN } =
  typeof WebSocket !== 'undefined'
    ? WebSocket
    : {
        OPEN: 1,
        CLOSED: 3,
      }

export class WebSocketChannelConnection extends ChannelConnection {
  get disconnected(): boolean {
    throw new Error('Method not implemented.')
  }
  get connected(): boolean {
    throw new Error('Method not implemented.')
  }
  whenConnected(): Promise<void> {
    throw new Error('Method not implemented.')
  }
  whenDisconnected(): Promise<void> {
    throw new Error('Method not implemented.')
  }
  _sendObject(obj: any): void {
    throw new Error('Method not implemented.')
  }
  protected _close(): void {
    throw new Error('Method not implemented.')
  }
  constructor(url: string | URL, options?: TransformOptions)
  constructor(url: string | URL, protocols?: string | string[], options?: TransformOptions)
  constructor(websocket: NativeWebSocket, options?: TransformOptions)
  constructor(websocket: SockJsWebSocket, options?: TransformOptions)

  constructor(arg0: any, arg1?: any, arg2?: any) {
    super()

    let webSocket: NativeWebSocket | SockJsWebSocket | undefined

    const { parse, stringify } = resolveTransformOptions(
      (arg2 && typeof arg2 === 'object' && !Array.isArray(arg2)) ||
        (arg1 && typeof arg1 === 'object' && !Array.isArray(arg1))
    )

    if (typeof arg0 === 'string' || arg0 instanceof URL) {
      const url = arg0
      // @ts-ignore
      const protocols: string | string[] =
        Array.isArray(arg1) || typeof arg1 === 'string' ? arg1 : undefined
      const args: [url: string | URL, protocols?: string | string[]] = protocols
        ? [url, protocols]
        : [url]
      webSocket = new WebSocket(...args)
    }

    if (!webSocket) {
      throw new TypeError('The "webSocket" argument must be required. Received type ' + typeof arg0)
    }

    const [connectedPromise, resolveConnected, disConnectedPromise, resolveDisconnected] =
      _baseConstruct.call(this)

    const onClose = () => {
      resolveDisconnected()
      // @ts-ignore
      this._w = undefined
      this.close()
    }

    const onError = (...args: any[]) => {
      this.emitError(args)
    }

    if (
      // @ts-ignore
      typeof webSocket['on'] === 'function' &&
      // @ts-ignore
      typeof webSocket['write'] === 'function'
    ) {
      const _webSocket = webSocket as SockJsWebSocket
      _webSocket.on('data', (message: string) => {
        try {
          this.emitObject(parse(message))
        } catch {}
      })
      _webSocket.on('error', onError)
      _webSocket.on('close', onClose)
      this._close = () => {
        resolveDisconnected()
        // @ts-ignore
        this._w = undefined
        _webSocket.close()
      }
      this._sendObject = (obj) => {
        _webSocket.write(stringify(obj))
      }
    } else if (
      // @ts-ignore
      typeof webSocket['addEventListener'] === 'function' &&
      // @ts-ignore
      typeof webSocket['send'] === 'function'
    ) {
      const _webSocket = webSocket as NativeWebSocket

      const onMessage = ({ data }: { data: any }) => {
        try {
          this.emitObject(typeof data === 'string' ? parse(data) : data)
        } catch {}
      }
      const onOpen = () => {
        resolveConnected()
      }

      _webSocket.addEventListener('message', onMessage)
      _webSocket.addEventListener('error', onError)
      _webSocket.addEventListener('open', onOpen)
      _webSocket.addEventListener('close', onClose)
      this._close = () => {
        resolveDisconnected()
        // @ts-ignore
        this._w = undefined
        _webSocket.removeEventListener('message', onMessage)
        _webSocket.removeEventListener('error', onError)
        _webSocket.removeEventListener('open', onOpen)
        _webSocket.removeEventListener('close', onClose)
        _webSocket.close()
      }
      this._sendObject = (obj) => {
        _webSocket.send(stringify(obj))
      }
    } else {
      throw new TypeError('This instance of websocket is not supported.')
    }

    switch (webSocket.readyState) {
      case WebSocket_OPEN:
        resolveConnected()
        break
      case WebSocket_CLOSED:
        resolveDisconnected()
        break
    }
  }
}

// @ts-ignore
type ElectronMessagePort = import('electron').MessagePortMain

// @ts-ignore
type NativeMessagePort = MessagePort | import('worker_threads').MessagePort
// @ts-ignore
type NodeWorker = import('worker_threads').Worker

function MessageChannelConnection_construct(
  this: ChannelConnection,
  params: ReturnType<typeof _baseConstruct>,
  port: NativeMessagePort | ElectronMessagePort | Worker | NodeWorker,
  closeOnDisconnected?: boolean
) {
  const [connectedPromise, resolveConnected, disConnectedPromise, resolveDisconnected] = params

  const onMessage = ({ data }: MessageEvent) => {
    try {
      this.emitObject(data)
    } catch {}
  }
  const onError = (...args: any[]) => {
    this.emitError(args)
  }
  const onClose = () => {
    resolveDisconnected()
    this.close()
  }

  // @ts-ignore
  if (typeof port.addEventListener === 'function') {
    const close = this.close.bind(this)

    registerOnExit(close)
    disConnectedPromise.then(() => {
      unregisterOnExit(close)
    })

    // @ts-ignore
    port.addEventListener('message', onMessage)
    // @ts-ignore
    port.addEventListener('messageerror', onError)

    this._close = () => {
      resolveDisconnected()
      // @ts-ignore
      port.removeEventListener('message', onMessage)
      // @ts-ignore
      port.removeEventListener('messageerror', onError)
      if (closeOnDisconnected) {
        // @ts-ignore
        if (typeof port.close === 'function') {
          // @ts-ignore
          port.close()
        }
        // @ts-ignore
        else if (typeof port.terminate === 'function') {
          // @ts-ignore
          port.terminate()
        }
      }
    }
    this._sendObject = (obj) => {
      // @ts-ignore
      port.postMessage(obj)
    }
  }
  // @ts-ignore
  else if (typeof port.on === 'function') {
    // @ts-ignore
    port.on('message', onMessage)
    // @ts-ignore
    port.on('error', onError)
    // @ts-ignore
    port.on('messageerror', onError)
    // @ts-ignore
    port.on('close', onClose)
    // @ts-ignore
    port.on('exit', onClose)

    this._close = () => {
      resolveDisconnected()
      // @ts-ignore
      port.off('message', onMessage)
      // @ts-ignore
      port.off('error', onError)
      // @ts-ignore
      port.off('messageerror', onError)
      // @ts-ignore
      port.off('close', onClose)
      // @ts-ignore
      port.off('exit', onClose)
      if (closeOnDisconnected) {
        // @ts-ignore
        port.close?.()
      }
    }
    this._sendObject = (obj) => {
      // @ts-ignore
      port.postMessage(obj)
    }
  } else {
    throw new TypeError('This instance of MessagePort is not supported.')
  }

  // @ts-ignore
  if (typeof port.start === 'function') {
    // @ts-ignore
    port.start()
  }

  resolveConnected()
}

export class MessageChannelConnection extends ChannelConnection {
  get disconnected(): boolean {
    throw new Error('Method not implemented.')
  }
  get connected(): boolean {
    throw new Error('Method not implemented.')
  }
  whenConnected(): Promise<void> {
    throw new Error('Method not implemented.')
  }
  whenDisconnected(): Promise<void> {
    throw new Error('Method not implemented.')
  }
  _sendObject(obj: any): void {
    throw new Error('Method not implemented.')
  }
  protected _close(): void {
    throw new Error('Method not implemented.')
  }
  constructor(
    port:
      | NativeMessagePort
      | ElectronMessagePort
      | Promise<NativeMessagePort | ElectronMessagePort>,
    closeOnDisconnected?: boolean
  ) {
    super()
    const params = _baseConstruct.call(this)

    Promise.resolve(port).then((port) =>
      MessageChannelConnection_construct.call(this, params, port, closeOnDisconnected)
    )
  }
}

// @ts-ignore
type ChildProcess = import('child_process').ChildProcess

export class IpcConnection extends ChannelConnection {
  get disconnected(): boolean {
    throw new Error('Method not implemented.')
  }
  get connected(): boolean {
    throw new Error('Method not implemented.')
  }
  whenConnected(): Promise<void> {
    throw new Error('Method not implemented.')
  }
  whenDisconnected(): Promise<void> {
    throw new Error('Method not implemented.')
  }
  _sendObject(obj: any): void {
    throw new Error('Method not implemented.')
  }
  protected _close(): void {
    throw new Error('Method not implemented.')
  }
  constructor(proc: ChildProcess | NodeJS.Process = process) {
    super()

    const [connectedPromise, resolveConnected, disConnectedPromise, resolveDisconnected] =
      _baseConstruct.call(this)

    const onMessage = (message: any) => {
      try {
        this.emitObject(message)
      } catch {}
    }
    const onError = (...args: any[]) => {
      this.emitError(args)
    }
    const onClose = () => {
      resolveDisconnected()
      this.close()
    }

    proc.on('message', onMessage)
    proc.on('disconnect', onClose)

    // @ts-ignore
    if (typeof proc.exit === 'function') {
      const _process = proc as NodeJS.Process
      _process.on('exit', onClose)

      this._close = () => {
        resolveDisconnected()
        _process.off('message', onMessage)
        _process.off('disconnect', onClose)
      }

      this._sendObject = (obj) => {
        _process.send?.(obj, undefined, undefined, (err) => {
          if (err) onError(err)
        })
      }

      if (_process.connected) {
        resolveConnected()
      } else {
        resolveDisconnected()
      }
    } else {
      const _child_process = proc as ChildProcess
      _child_process.on('close', onClose)
      _child_process.on('exit', onClose)
      _child_process.on('error', onError)

      this._close = () => {
        resolveDisconnected()
        _child_process.off('message', onMessage)
        _child_process.off('disconnect', onClose)
        _child_process.off('error', onError)
        _child_process.off('close', onClose)
        _child_process.off('exit', onClose)
      }

      this._sendObject = (obj) => {
        _child_process.send?.(obj, undefined, undefined, (err) => {
          if (err) onError(err)
        })
      }

      if (!_child_process.channel) {
        resolveDisconnected()
      } else if (_child_process.connected) {
        resolveConnected()
      } else {
        _child_process.once('spawn', resolveConnected!)
      }
    }
  }
}
const getParentPort = /* #__PRUE__ */ once(async () => (await import('worker_threads')).parentPort)
// @ts-ignore
const isNode =
  /* #__PRUE__ */ typeof process !== 'undefined' &&
  typeof process.versions === 'object' &&
  typeof process.versions.node === 'string'
// @ts-ignore
const isWebWorker = /*#__PURE__*/ typeof importScripts === 'function' && typeof self !== 'undefined'

export class WorkerConnection extends ChannelConnection {
  get disconnected(): boolean {
    throw new Error('Method not implemented.')
  }
  get connected(): boolean {
    throw new Error('Method not implemented.')
  }
  whenConnected(): Promise<void> {
    throw new Error('Method not implemented.')
  }
  whenDisconnected(): Promise<void> {
    throw new Error('Method not implemented.')
  }
  _sendObject(obj: any): void {
    throw new Error('Method not implemented.')
  }
  protected _close(): void {
    throw new Error('Method not implemented.')
  }
  constructor(
    // @ts-ignore
    workerOrWorkerSelf?:
      | SharedWorker
      | Worker
      | NodeWorker
      | Promise<SharedWorker | Worker | NodeWorker>,
    closeOnDisconnected?: boolean
  ) {
    super()
    const params = _baseConstruct.call(this)

    Promise.resolve(workerOrWorkerSelf)
      .then((obj: any) => {
        if (obj) {
          if (typeof obj.port === 'object' && typeof obj.addEventListener === 'function') {
            return (obj as SharedWorker).port
          } else if (typeof obj.postMessage === 'function' && typeof obj.terminate === 'function') {
            return obj as Worker | NodeWorker
          }
        }
      })
      .then((port) => {
        if (port) {
          MessageChannelConnection_construct.call(this, params, port, closeOnDisconnected)
        } else {
          const [connectedPromise, resolveConnected, disConnectedPromise, resolveDisconnected] =
            params

          const onError = (...args: any[]) => {
            this.emitError(args)
          }
          const onClose = () => {
            resolveDisconnected()
            this.close()
          }

          if (isNode) {
            const onMessage = (message: any) => {
              try {
                this.emitObject(message)
              } catch {}
            }

            process.on('message', onMessage)
            process.on('disconnect', onClose)
            process.on('exit', onClose)

            this._close = () => {
              resolveDisconnected()
              process.off('message', onMessage)
              process.off('disconnect', onClose)
            }

            this._sendObject = (obj) => {
              process.send?.(obj, undefined, undefined, (err) => {
                if (err) onError(err)
              })
            }

            if (process.connected) {
              resolveConnected()
            } else {
              resolveDisconnected()
            }
          } else if (isWebWorker) {
            const onMessage = ({ data }: MessageEvent) => {
              try {
                this.emitObject(data)
              } catch {}
            }

            self.addEventListener('message', onMessage)
            self.addEventListener('messageerror', onError)
            self.addEventListener('unload', onError)

            this._close = () => {
              resolveDisconnected()
              self.removeEventListener('message', onMessage)
              self.removeEventListener('messageerror', onError)
              self.removeEventListener('unload', onError)
            }

            this._sendObject = (obj) => {
              self.postMessage(obj)
            }

            resolveConnected()
          } else {
            throw new Error('unkown environment.')
          }
        }
      })
  }
}
